package com.timk.goserver.client.board;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import com.timk.goserver.client.compatibility.Point;

/**
 * This class represents the board, stones, and markup.
 * @author TKington
 *
 */
public class Board // NOPMD - Complexity
{
	/** No mark */
	public static final int MARK_NONE = 0;
	/** Current move */
	public static final int MARK_CURMOVE = 1;
	/** Black territory */
	public static final int MARK_TERRBLACK = 2;
	/** White territory */
	public static final int MARK_TERRWHITE = 3;
	/** Undecided territory */
	public static final int MARK_TERRUNKNOWN = 4;
	
	/** The x,y offsets of a stone's liberties. */
    public static int [][] offsets = {{0, 1}, {1, 0}, {-1, 0}, {0, -1}};
    
    /** The stones on the board.  1 for black, -1 for white. */
    private int [][] grid;
    /** Markings on the board - current stone, etc. */
    private int [][] marks;
    /** The size of the board. */
    private final int size;
    
    /** The point, if any, where it is illegal to play on the next move. */
    private Point koPoint = new Point(-1, -1);
    
    private final List listeners = new ArrayList();
    
    /**
     * Creates a Board object of the given size.
     * @param size the size
     */
    public Board(int size)
    {
        this.size = size;
        grid = new int[size][size];
        marks = new int[size][size];
    }
    
    /**
     * Checks to see if a given move is legal.
     * @param color 1 for black, -1 for white
     * @param x the x location
     * @param y the y location
     * @return true if the move is legal
     */
    public boolean isLegalMove(int color, int x, int y) { // NOPMD - Complexity
        if(x < 0 || x >= size || y < 0 || y >= size || grid[x][y] != 0) {
			return false;
		}
        
        if(koPoint != null && x == koPoint.x && y == koPoint.y) {
			return false;
		}
        
        try {
            grid[x][y] = color;
            
            if(countLiberties(x, y) > 0) {
				return true;
			}
            
            for(int i = 0; i < offsets.length; i++) {
                int nx = x + offsets[i][0];
                int ny = y + offsets[i][1];
                if(nx < 0 || nx >= size || ny < 0 || ny >= size) {
					continue;
				}
                
                if(grid[nx][ny] == color * -1 && countLiberties(nx, ny) == 0) {
					return true;
				}
            }
            
            return false;
        }
        finally {
            grid[x][y] = 0;
        }
    }
    
    /**
     * Plays a move on the Board.
     * @param color 1 for black, -1 for white
     * @param location the x,y coordinates
     * @param removedStones list that will be filled with 
     *   the locations of any stones that were captured by making the move
     */
    public void move(int color, Point location, Set removedStones) { // NOPMD - Complexity
        int x = location.x;
        int y = location.y;
        
    	grid[x][y] = color;
        
        for(int i = 0; i < offsets.length; i++) {
            int nx = x + offsets[i][0];
            int ny = y + offsets[i][1];
            if(nx < 0 || nx >= size || ny < 0 || ny >= size) {
				continue;
			}
            
            if(grid[nx][ny] == color * -1 && countLiberties(nx, ny) == 0) {
                removeGroup(nx, ny, removedStones);
            }
        }
        
        if(removedStones.size() == 1 && isAlone(x, y) && countLiberties(x, y) == 1) {
            setKoPoint(x, y);
        }
        else {
			koPoint = null;
		}
    }
    
    /**
     * Removes the group of stones connected to the stone at x,y, and adds
     * their locations to removedStones.
     * @param x the x location
     * @param y the y location
     * @param removedStones list that will be filled with the
     *   locations of the removed stones
     */
    private void removeGroup(int x, int y, Set removedStones) {
        int color = grid[x][y];
        
        List q = new ArrayList();
        q.add(new Point(x, y));
        
        while(!q.isEmpty()) {
            Point p = (Point)q.remove(0);
            
            for(int i = 0; i < offsets.length; i++) {
                int nx = p.x + offsets[i][0];
                int ny = p.y + offsets[i][1];
                if(nx < 0 || nx >= size || ny < 0 || ny >= size) {
					continue;
				}
                
                if(grid[nx][ny] == color) {
					q.add(new Point(nx, ny));
				}
            }
            
            removedStones.add(new Point(p.x, p.y));
            grid[p.x][p.y] = 0;
        }
    }
    
    /**
     * Checks whether a stone is connected to any stones of the same color.
     * @param x the x location
     * @param y the y location
     * @return true if the stone is alone
     */
    private boolean isAlone(int x, int y) {
        int color = grid[x][y];
        
        for(int i = 0; i < offsets.length; i++) {
            int nx = x + offsets[i][0];
            int ny = y + offsets[i][1];
            if(nx < 0 || nx >= size || ny < 0 || ny >= size) {
				continue;
			}
            
            if(grid[nx][ny] == color) {
				return false;
			}
        }
        
        return true;
    }
    
    /**
     * Sets the ko point on the Board.  That is, sets the location where a stone
     * cannot be played on the next move.  The x and y coordinates that are passed
     * in are those of the last stone played, and the ko point will be the only
     * empty neighboring intersection. 
     * @param x the x location of the last stone played
     * @param y the y location of the last stone played
     */
    private void setKoPoint(int x, int y) {
        for(int i = 0; i < offsets.length; i++) {
            int nx = x + offsets[i][0];
            int ny = y + offsets[i][1];
            if(nx < 0 || nx >= size || ny < 0 || ny >= size) {
				continue;
			}
            
            if(grid[nx][ny] == 0) {
            	koPoint = new Point(nx, ny);
                return;
            }       
        }
    }
    
    /**
     * Counts the liberties of the string of stones at x,y.
     * @param x the x location
     * @param y the y location
     * @return the number of liberties on the string
     */
    private int countLiberties(int x, int y) { // NOPMD - Complexity
        int color = grid[x][y];
        boolean [][] used = new boolean[size][size];
        
        ArrayList q = new ArrayList();
        q.add(new Point(x, y));
        
        int num = 0;
        while(!q.isEmpty()) {
            Point p = (Point)q.remove(0);
            used[p.x][p.y] = true;
            
            for(int i = 0; i < offsets.length; i++) {
                int nx = p.x + offsets[i][0];
                int ny = p.y + offsets[i][1];
                if(nx < 0 || nx >= size || ny < 0 || ny >= size) {
					continue;
				}
                
                if(grid[nx][ny] == color && !used[nx][ny]) {
					q.add(new Point(nx, ny));
				}
                else if(grid[nx][ny] == 0 && !used[nx][ny]) {
                    num++;
                    used[nx][ny] = true;
                }
            }
        }
        
        return num;
    }
    
    /**
     * Removes the stone at x,y
     * @param p the x,y location
     * @return the color of the stone
     */
    public int remove(Point p) {
        int old = grid[p.x][p.y];
        grid[p.x][p.y] = 0;
        return old;
    }
    
    /**
     * Gets the stone at x,y on the Board.
     * @param x the x location
     * @param y the y location
     * @return 1 for black, 0 for empty, -1 for white
     */
    public int getAt(int x, int y) { return grid[x][y]; }
    
    /**
     * Gets the stone at x,y on the Board.
     * @param p the x,y location
     * @return 1 for black, 0 for empty, -1 for white
     */
    public int getAt(Point p) { return grid[p.x][p.y]; }
    
    /**
     * Sets the stone at x,y on the Board.
     * @param p the x,y location
     * @param color 1 for black, 0 for empty, -1 for white
     */
    public void setAt(Point p, int color) { grid[p.x][p.y] = color; }
    
    /**
     * Returns the mark at x,y on the Board.
     * @param x the x location
     * @param y the y location
     * @return the mark
     */
    public int getMarkAt(int x, int y) { return marks[x][y]; }
    
    /**
     * Sets the mark at x,y on the Board.
     * @param p the x,y location
     * @param mark the mark
     * @return the previous mark at the location
     */
    public int setMarkAt(Point p, int mark) {
    	return setMarkAt(p.x, p.y, mark);
    }
    
    /**
     * Sets the mark at x,y on the Board
     * @param x the x location
     * @param y the y location
     * @param mark the mark
     * @return the previous mark at this location
     */
    public int setMarkAt(int x, int y, int mark) {
    	int oldMark = marks[x][y];
    	marks[x][y] = mark;
    	return oldMark;
    }
    
    /**
     * Returns the size of the Board.
     * @return the size of the Board
     */
    public int getSize() { return size; }
    
    /**
     * Returns the ko point - the point where it is illegal to play the next move,
     * if any.
     * @return the ko point
     */
    public Point getKoPoint() { return koPoint; }
    
    /**
     * Sets the ko point - the point where it is illegal to play the next move.
     * @param p the ko point
     */
    public void setKoPoint(Point p) {
    	if(p == null) {
			koPoint = null;
		} else {
			koPoint = new Point(p);
		}
    }
    
    /**
     * Adds a listener
     * @param l the BoardListener
     */
    public void addListener(BoardListener l) { listeners.add(l); }
    
    /**
     * Removes a listener
     * @param l the BoardListener
     */
    public void removeListener(BoardListener l) { listeners.remove(l); }
    
    /**
     * Fires a repaint event to the BoardPanel
     *
     */
    public void fireRepaint() {
    	for(int i = 0; i < listeners.size(); i++) {
    		BoardListener l = (BoardListener)listeners.get(i);
    		l.boardChanged(this);
    	}
    }
}
